第13章 数据聚类与分群分析¶
非监督式学习的数据集只有特征变量, 而没有目标变量, 我们需要对已有数据进行建模, 根据性质进行分组, 其典型案例就是聚类分析问题
这章讲解聚类分析的两种常见算法--KMeans和DBSCAN算法
13.1 KMeans算法¶
13.1.1 KMeans算法的基本原理¶
KMeans算法名称中的K代表类别数量, Means代表每个类别内样本的均值, 所以KMeans算法又称为K-均值算法
KMeans算法以距离作为样本间相似度的度量标准, 将距离相近的样本分配至同一个类别, 样本间距离的计算方式可以是欧式距离、曼哈顿距离、余弦相似度等, KMeans算法通常采用欧式距离来度量各样本间的距离
KMeans算法的核心思想是对每个样本点计算到各个中心点的距离, 并将该样本点分配给距离最近的中心点代表的类别, 一次迭代完成后, 根据聚类结果更新每个类别的中心点, 然后重复之前的操作再次迭代, 直到前后两次分类结果没有差别
13.1.2 KMeans算法的代码实现¶
import warnings
warnings.filterwarnings('ignore')
import numpy as np
data = np.array([[3, 2], [4, 1], [3, 6], [4, 7], [3, 9], [6, 8], [6, 6], [7, 7]])
data
array([[3, 2], [4, 1], [3, 6], [4, 7], [3, 9], [6, 8], [6, 6], [7, 7]])
import matplotlib.pyplot as plt
plt.scatter(data[:, 0], data[:, 1], c="red", marker='o', label='samples') # 以红色圆圈样式绘制散点图并加上标签
plt.legend() # 设置图例,图例内容为上面设置的label参数
plt.show()
from sklearn.cluster import KMeans
kms = KMeans(n_clusters=2)
kms.fit(data)
KMeans(n_clusters=2)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
KMeans(n_clusters=2)
# 获取结果
label = kms.labels_
print(label)
[0 0 1 1 1 1 1 1]
# 结果可视化
plt.scatter(data[label == 0][:, 0], data[label == 0][:, 1], c="red", marker='o', label='class0') # 以红色圆圈样式绘制散点图并加上标签
plt.scatter(data[label == 1][:, 0], data[label == 1][:, 1], c="green", marker='*', label='class1') # 以绿色星星样式绘制散点图并加上标签
plt.legend() # 设置图例
<matplotlib.legend.Legend at 0x1ae55b319a0>
# 聚成3类
kms_3 = KMeans(n_clusters=3)
kms_3.fit(data)
label_3 = kms_3.labels_
print(label_3)
[1 1 0 0 0 2 2 2]
plt.scatter(data[label_3 == 0][:, 0], data[label_3 == 0][:, 1], c="red", marker='o', label='class0') # 以红色圆圈样式绘制散点图并加上标签
plt.scatter(data[label_3 == 1][:, 0], data[label_3 == 1][:, 1], c="green", marker='*', label='class1') # 以绿色星星样式绘制散点图并加上标签
plt.scatter(data[label_3 == 2][:, 0], data[label_3 == 2][:, 1], c="blue", marker='+', label='class2') # 以蓝色加号样式绘制散点图并加上标签
plt.legend() # 设置图例
<matplotlib.legend.Legend at 0x1ae36dcc950>
13.1.3 案例实战: 银行客户分群模型¶
银行拥有海量的客户,对于不同的客户,银行需要采取不同的营销工作策略
例如,对于收入高且风险承受能力强的客户,可以重点挖掘业务机会,如向其推销一些收益率高但周期相对较长的理财产品;而对于收入低且风险承受能力较弱的客户,则需要采取其他策略
因此,银行通常需要将客户进行分群处理,以便有的放矢地开展营销工作
import pandas as pd
data = pd.read_excel('客户信息.xlsx')
data.head()
年龄(岁) | 收入(万元) | |
---|---|---|
0 | 50 | 66 |
1 | 44 | 51 |
2 | 30 | 56 |
3 | 46 | 50 |
4 | 32 | 50 |
import matplotlib.pyplot as plt
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c="green", marker='*') # 以绿色星星样式绘制散点图
plt.xlabel('age') # 添加x轴名称
plt.ylabel('salary') # 添加y轴名称
plt.show()
from sklearn.cluster import KMeans
kms = KMeans(n_clusters=3, random_state=123)
kms.fit(data)
label = kms.labels_
label = kms.fit_predict(data)
print(label)
[1 1 2 1 2 2 1 2 2 1 1 1 1 2 1 1 1 2 1 1 1 2 2 1 1 1 1 2 2 1 2 1 2 2 2 0 2 1 2 0 1 1 2 1 2 1 2 1 1 2 2 0 1 2 1 1 1 1 2 1 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 1 1 1 2 1 2 0 0 0 0 0 0 2]
plt.scatter(data[label == 0].iloc[:, 0], data[label == 0].iloc[:, 1], c="red", marker='o', label='class0') # 以红色圆圈样式绘制散点图并加上标签
plt.scatter(data[label == 1].iloc[:, 0], data[label == 1].iloc[:, 1], c="green", marker='*', label='class1') # 以绿色星星样式绘制散点图并加上标签
plt.scatter(data[label == 2].iloc[:, 0], data[label == 2].iloc[:, 1], c="blue", marker='+', label='class2') # 以蓝色加号样式绘制散点图并加上标签
plt.xlabel('age') # 添加x轴名称
plt.ylabel('salary') # 添加y轴名称
plt.legend() # 设置图例
<matplotlib.legend.Legend at 0x1ae57fc4800>
print(data[label == 0].iloc[:, 1].mean()) # 看下分类为标签0的人的收入均值,iloc[:, 1]为data表格的第二列,也即“收入”列
print(data[label == 1].iloc[:, 1].mean())
print(data[label == 2].iloc[:, 1].mean())
21.125 57.55555555555556 46.285714285714285
13.2 DBSCAN算法¶
DBSCAN(Density-Based Spatial Clustering ofApplications with Noise)是一种以密度为基础的空间聚类算法,可以用密度的概念剔除不属于任一类别的噪声点
该算法将簇定义为密度相连的点的最大集合,将具有足够密度的区域划分为簇,并可以发现任意形状的簇
13.2.1 DBSCAN算法的基本原理¶
一个案例
一个可视化网站
打开网站后选择 "Smiley Face" 类的数据, 参数 "epsilon" 代表画圆半径, 参数 "minPoints" 代表圆内最小样本数, 可自主调节, 画圆半径越小、圆内最小样本数越大, 对于簇的产生越严格、密度要求越高、离群点越多
点击 "Go!" 按钮开始演示DBSCAN算法的聚类过程
这里放一个演示
13.2.2 DBSCAN算法的代码实现及与KMeans的对比¶
import pandas as pd
data = pd.read_excel('演示数据.xlsx')
data.head()
x | y | |
---|---|---|
0 | 10.44 | 5.74 |
1 | 11.55 | 6.16 |
2 | 11.36 | 5.10 |
3 | 10.62 | 6.12 |
4 | 11.20 | 5.39 |
import matplotlib.pyplot as plt
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c="green", marker='*') # 以绿色星星样式绘制散点图
plt.xlabel('x') # 添加x轴名称
plt.ylabel('y') # 添加y轴名称
plt.show()
DBSCAN¶
from sklearn.cluster import DBSCAN
dbs = DBSCAN()
dbs.fit(data)
label_dbs = dbs.labels_
print(label_dbs)
[0 0 1 0 1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 1 1 0 1 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 0 0 0 1 0 1 0 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 0 1 1 0 0 0 0 0 1 1 0 0 0 1 1 1 1 1 1 0 1 0 0 1 0 0 1 0 1 1 1 1 0 1 1 1 0 1 1 0]
plt.scatter(data[label_dbs == 0].iloc[:, 0], data[label_dbs == 0].iloc[:, 1], c="red", marker='o', label='class0') # 以红色圆圈样式绘制散点图并加上标签
plt.scatter(data[label_dbs == 1].iloc[:, 0], data[label_dbs == 1].iloc[:, 1], c="green", marker='*', label='class1') # 以绿色星星样式绘制散点图并加上标签
plt.xlabel('x') # 添加x轴名称
plt.ylabel('y') # 添加y轴名称
plt.legend() # 设置图例
<matplotlib.legend.Legend at 0x1ae591adb80>
KMeans¶
from sklearn.cluster import KMeans
KMs = KMeans(n_clusters=2)
KMs.fit(data)
label_kms = KMs.labels_
# KMs # 这样可以查看模型参数,这里没有设置random_state参数,所以可能每次跑出来的结果略有不同(因为每次起始点选的地方不同)
print(label_kms)
[1 1 0 1 1 1 0 0 0 0 0 0 0 1 1 0 1 1 0 1 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 0 0 1 1 1 1 1 0 0 0 1 0 1 0 0 1 0 1 0 0 1 1 1 0 0 0 1 0 0 0 0 1 0 1 0 1 0 1 1 0 1 0 0]
plt.scatter(data[label_kms == 0].iloc[:, 0], data[label_kms == 0].iloc[:, 1], c="red", marker='o', label='class0') # 以红色圆圈样式绘制散点图并加上标签
plt.scatter(data[label_kms == 1].iloc[:, 0], data[label_kms == 1].iloc[:, 1], c="green", marker='*', label='class1') # 以绿色星星样式绘制散点图并加上标签
plt.xlabel('x') # 添加x轴名称
plt.ylabel('y') # 添加y轴名称
plt.legend() # 设置图例
<matplotlib.legend.Legend at 0x1ae36e727b0>
可以看到,对于形状类似同心圆的数据,KMeans算法聚类效果较差,只能机械地将数据分为左右两部分,而无法以外圆内圆的方式进行区分
13.3 案例实战: 新闻聚类分群模型¶
13.3.1 案例背景¶
新闻种类繁复多样,可以分为财经、体育、科技、娱乐等题材
在本案例中,根据10个关键词从百度新闻爬取了962条新闻(具体方法可以参考本章最后的第2个“补充知识点”),且每个关键词对应的新闻条数相近,现在需要通过Python编程对每条新闻划分类别,匹配到正确的版面,这实际上就是在对新闻进行聚类分群
13.3.2 文本数据的读取与处理¶
在进行新闻聚类分群之前,需要读取新闻数据
新闻数据是文本类型的,并不能直接用于模型训练,所以读取数据之后还需要将文本类型的数据处理成数值类型的数据
import pandas as pd
df = pd.read_excel('新闻.xlsx')
df.head()
关键词 | 标题 | 网址 | 来源 | 时间 | |
---|---|---|---|---|---|
0 | 华能信托 | 信托公司2019年上半年经营业绩概览 | http://www.financialnews.com.cn/jrsb_m/xt/zx/2... | 中国金融新闻网 | 2019年07月23日 00:00 |
1 | 华能信托 | 首单信托型企业ABS获批 | http://www.jjckb.cn/2018-10/23/c_137552198.htm | 经济参考网 | 2018年10月23日 12:21 |
2 | 华能信托 | 华能贵诚信托孙磊:金融科技助力打造开放信托生态 | https://baijiahao.baidu.com/s?id=1639276579449... | 同花顺财经 | 2019年07月17日 10:49 |
3 | 华能信托 | 华能贵诚信托孙磊:金融科技已经成为信托行业重要的基础设施 | https://finance.qq.com/a/20190716/007898.htm | 腾讯财经 | 2019年07月16日 18:53 |
4 | 华能信托 | 格力电器股权转让意向方闭门开会 华能信托赫然在列 | https://finance.sina.com.cn/trust/roll/2019-05... | 新浪 | 2019年05月22日 22:53 |
在前面的章节中出现的数据可以分为连续型(如0~100的连续值)和离散型(如样本的类别0和1)两类,但还有一类数据是文本数据,如本节中的新闻标题文本
这种文本类型的数据需要转换为数值类型的数据才能在Python中处理
这项工作需要用到两个核心技术——中文分词和文本向量化
中文分词:
中文分词就是将一句话拆分成一些词语,例如,“我爱北京天安门”就可以拆分成“我”“爱”“北京”“天安门”
中文分词后就可以进行文本向量化,搭建词频矩阵,从而将文本转换为数字
在Python中有专门进行中文分词的jieba库,这里主要讲解jieba库的核心知识点,jieba库的更多扩展知识见本章最后的第1个“补充知识点”
2 中文分词¶
# 如果没有安装jieba库,可以将下面代码取消注释后运行,即可安装jieba库
# !pip install jieba
import warnings
warnings.filterwarnings('ignore')
# 中文分词演示
import jieba
word = jieba.cut('我爱北京天安门')
for i in word:
print(i)
Building prefix dict from the default dictionary ... Loading model from cache C:\Users\19559\AppData\Local\Temp\jieba.cache Loading model cost 0.435 seconds. Prefix dict has been built successfully.
我 爱 北京 天安门
# 第一条新闻标题
df.iloc[0]['标题']
'信托公司2019年上半年经营业绩概览'
# 第一条新闻标题中文分词
import jieba
word = jieba.cut(df.iloc[0]['标题'])
result = ' '.join(word)
print(result)
信托公司 2019 年 上半年 经营 业绩 概览
# 通过for循环遍历来进行所有标题的分词
import jieba
words = []
for i, row in df.iterrows():
word = jieba.cut(row['标题']) # 这里的word是一个generator
result = ' '.join(word) # 单引号里的是分隔符, 这里将word变为一个str
words.append(result)
# generator
# 生成器表达式
gen_expr = (i * 2 for i in range(5))
print(type(gen_expr))
# 遍历生成器
for num in gen_expr:
print(num)
<class 'generator'> 0 2 4 6 8
words[0:3] # 展示前三条新闻的分词结果
['信托公司 2019 年 上半年 经营 业绩 概览', '首单 信托 型 企业 ABS 获批', '华能 贵 诚信 托孙磊 : 金融 科技 助力 打造 开放 信托 生态']
# 熟悉了上面的过程后,可以把代码合并写成如下形式
import jieba
words = []
for i, row in df.iterrows():
words.append(' '.join(jieba.cut(row['标题'])))
print(len(words))
962
words[0:3] # 同样展示前三条新闻的分词结果
['信托公司 2019 年 上半年 经营 业绩 概览', '首单 信托 型 企业 ABS 获批', '华能 贵 诚信 托孙磊 : 金融 科技 助力 打造 开放 信托 生态']
补充知识点:遍历DataFrame表格的函数 - iterrows()函数¶
for i, row in df[0:5].iterrows():
print(i)
print(row)
0 关键词 华能信托 标题 信托公司2019年上半年经营业绩概览 网址 http://www.financialnews.com.cn/jrsb_m/xt/zx/2... 来源 中国金融新闻网 时间 2019年07月23日 00:00 Name: 0, dtype: object 1 关键词 华能信托 标题 首单信托型企业ABS获批 网址 http://www.jjckb.cn/2018-10/23/c_137552198.htm 来源 经济参考网 时间 2018年10月23日 12:21 Name: 1, dtype: object 2 关键词 华能信托 标题 华能贵诚信托孙磊:金融科技助力打造开放信托生态 网址 https://baijiahao.baidu.com/s?id=1639276579449... 来源 同花顺财经 时间 2019年07月17日 10:49 Name: 2, dtype: object 3 关键词 华能信托 标题 华能贵诚信托孙磊:金融科技已经成为信托行业重要的基础设施 网址 https://finance.qq.com/a/20190716/007898.htm 来源 腾讯财经 时间 2019年07月16日 18:53 Name: 3, dtype: object 4 关键词 华能信托 标题 格力电器股权转让意向方闭门开会 华能信托赫然在列 网址 https://finance.sina.com.cn/trust/roll/2019-05... 来源 新浪 时间 2019年05月22日 22:53 Name: 4, dtype: object
pandas库中的iterrows()函数用于遍历DataFrame的每一行
可以看到,这里的i就是每一行的行索引序号,row就是每一行的内容,该内容是一个一维的Series对象,它可以根据索引来提取内容,例如,通过row['标题']可以提取该条新闻的标题内容,通过row['网址']可以提取该条新闻的网址内容
3 文本向量化基础: 建立词频矩阵¶
此时已经把每一条新闻标题分词完毕并存储到words列表中,下面需要将这些文本类型的数据转换为数值类型的数据,以便构造特征变量及训练模型
Python中有一个文本向量化函数CountVectorizer(),通过它可以很方便地将文本转换成数值,演示代码如下
# CountVectorizer()函数简单演示
from sklearn.feature_extraction.text import CountVectorizer
test = ['金融 科技 厉害', '华能 信托 厉害']
vect = CountVectorizer()
X = vect.fit_transform(test)
X = X.toarray()
print(X)
[[0 0 1 1 1] [1 1 1 0 0]]
CountVectorizer()函数会先根据空格来识别每一句话中的词语,例如,它能从第1条新闻标题中识别出“金融”“科技”“厉害”这3个词,从第2条新闻标题中识别出“华能”“信托”“厉害”这3个词,这2条标题一共能识别出“金融”“科技”“华能”“信托”“厉害”这5个不同的词
这5个词便构成了这2条新闻标题的词袋,CountVectorizer()函数会自动对词袋中的词进行编号,通过vocabulary_属性便能获取词袋的内容及相应编号
如果一个词出现了不止一次,例如,在某个标题中“金融”这个词出现了2次,那么其对应的值就是2
此外,CountVectorizer()函数会自动过滤掉一个字的词,这样会过滤掉“的”“之”等没有重要含义的词,不过同时也会过滤掉“爱”“坑”等可能有重要含义的词,因此,这个特点既是一个优势,也是一个劣势
# 查看词袋和对应的顺序
words_bag = vect.vocabulary_
print(words_bag)
{'金融': 4, '科技': 3, '厉害': 2, '华能': 1, '信托': 0}
print(words_bag.keys())
dict_keys(['金融', '科技', '厉害', '华能', '信托'])
可以看到,词袋是一个字典,每个词是字典的键,词对应的编号是字典的值
这些不同的词其实就代表着不同的特征,第几个编号就代表第几个特征
# 这样给词频矩阵和对应词放在一个表格中
wordBag = pd.DataFrame(columns= words_bag.keys(), data = X)
wordBag
金融 | 科技 | 厉害 | 华能 | 信托 | |
---|---|---|---|---|---|
0 | 0 | 0 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 0 | 0 |
4 文本向量化实战: 构造特征变量¶
# 将之前所有的新闻标题进行文本向量化
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer()
X = vect.fit_transform(words)
X = X.toarray()
# 查看所有新闻标题的词袋
words_bag = vect.vocabulary_
words_bag_20 = list(words_bag.items())[:20]
print(words_bag_20)
[('信托公司', 630), ('2019', 21), ('上半年', 296), ('经营', 2659), ('业绩', 345), ('概览', 2130), ('首单', 3337), ('信托', 628), ('企业', 538), ('abs', 103), ('获批', 2827), ('华能', 896), ('诚信', 2947), ('托孙磊', 1721), ('金融', 3199), ('科技', 2541), ('助力', 854), ('打造', 1720), ('开放', 1553), ('生态', 2408)]
wordBag = pd.DataFrame(data = X, columns= words_bag.keys())
wordBag.tail().iloc[:,10:20]
获批 | 华能 | 诚信 | 托孙磊 | 金融 | 科技 | 助力 | 打造 | 开放 | 生态 | |
---|---|---|---|---|---|---|---|---|---|---|
957 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
958 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
959 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
960 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
961 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
# 查看词袋中词的数目
len(words_bag)
3402
print(words[0])
print(words[1])
信托公司 2019 年 上半年 经营 业绩 概览 首单 信托 型 企业 ABS 获批
# 文本向量化
vect = CountVectorizer() # 引入CountVectorizer()函数
X_test = vect.fit_transform(words[0:2]) # 将前两条新闻文本向量化
X_test = X_test.toarray() # 将X_test转为数组
print(X_test) # 查看生成的二维数组
[[1 0 1 1 0 0 1 1 1 0 0] [0 1 0 0 1 1 0 0 0 1 1]]
# 查看词袋的第一种方式
words_bag = vect.vocabulary_ # 第一种查看词袋的方式
print(words_bag) # 查看词袋
{'信托公司': 6, '2019': 0, '上半年': 2, '经营': 8, '业绩': 3, '概览': 7, '首单': 10, '信托': 5, '企业': 4, 'abs': 1, '获批': 9}
# 将之前所有的新闻标题进行文本向量化并通过pandas展示
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
# 文本向量化
vect = CountVectorizer()
X = vect.fit_transform(words) # 将分词后的内容文本向量化
X = X.toarray()
# 查看文本向量化的结果
words_bag = vect.vocabulary_ # 第二种查看词袋的方法
df = pd.DataFrame(X, columns=words_bag)
df.head().iloc[:,30:40]
开会 | 赫然 | 直击 | 投资者 | 见面会 | 参会者 | 背后 | 国务院 | 国资委 | 召开 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
print(df.sum(axis = 1)[0:5]) # 按行求和
0 6 1 5 2 10 3 11 4 9 dtype: int64
13.3.3 模型的搭建与使用¶
1. 通过KMeans算法进行聚类分群¶
from sklearn.cluster import KMeans
kms = KMeans(n_clusters=10, random_state=123)
k_data = kms.fit_predict(df)
print(k_data[0:30])
[7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7]
其中每个数字代表一个分类, 可以看到, KMeans算法将新闻标题分成了10类
import numpy as np
words_ary = np.array(words)
print(words_ary[k_data == 8][0:5]) # 可以改成其他数字看看效果
['数据 科学 哪家 强 ? Python 和 R 的 对决 - 博客园 新闻 手机 版' '数字化 阅读 空间 | 怎么 用 Python 迅速 获取 网站 数据 ?' '今天 破解 了 压缩文件 的 密码 : 使用 python 轻松 编写 破解 程序' '程序员 如何 利用 Python 解决 女朋友 不看 天气 的 坏习惯 ?' '向 Excel 说 再见 , 神级 编辑器 统一 表格 与 Python']
2. 通过DBSCAN算法进行聚类分群¶
from sklearn.cluster import DBSCAN
dbs = DBSCAN(eps=1, min_samples=3) # 设置模型的eps参数(画圆半径)为1,min_samples参数(圆内最小样本数)为 3
d_data = dbs.fit_predict(df)
打印输出d_data来查看DBSCAN算法的聚类结果,如下所示
可以看出,DBSCAN算法对新闻标题的聚类效果较差,其中有大量离群点(-1),即不知道这条新闻标题属于什么分类
这是因为在13.3.2小节进行文本向量化后,每个新闻标题都有3401个特征,过多的特征容易导致样本点间的距离较远,从而产生离群点
因此,对于新闻文本而言,KMeans算法的聚类效果很好,DBSCAN算法的聚类效果则不尽如人意,这也说明了对于特征变量较多的数据,KMeans算法的聚类效果要优于DBSCAN算法的聚类效果
print(d_data[0:20])
[-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
13.3.4 模型优化¶
上一小节用KMeans算法搭建的模型已经能够较好地进行聚类分群,不过其还有可以优化的地方
例如,本案例的原始数据是根据10个关键词爬取的962条新闻,且每类新闻的条数是相近的,因此,最理 想的聚类结果是分为10类且每类数据的数量约为100个,而上面的聚类结果中很多新闻都被分类为8,分类显得很不均衡,离这一目标还有些差距
1. 模型误差产生的原因¶
产生这一差距的主要原因是新闻标题长短不一,在进行中文分词及文本向量化后,长标题和短标题的距离就较远,而KMeans模型是根据欧氏距离进行聚类分群的,因此容易造成含义相近的长标题和短标题却被分到不同的类别中
用如下所示的3条新闻标题进行演示,其中第3条新闻标题就是把第2条新闻标题重复了2遍
words_test = ['想去 华能 信托', '华能 信托 很好 想去', '华能 信托 很好 想去 华能 信托 很好 想去']
# 文本向量化
vect = CountVectorizer()
X_test = vect.fit_transform(words_test) # 将分词后的内容文本向量化
X_test = X_test.toarray()
# 查看文本向量化的结果
words_bag = vect.vocabulary_ # 第一种查看词袋的方法
df_test = pd.DataFrame(X_test, columns=words_bag)
df_test.head()
想去 | 华能 | 信托 | 很好 | |
---|---|---|---|---|
0 | 1 | 1 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
2 | 2 | 2 | 2 | 2 |
从含义来说,第2条新闻标题和第3条新闻标题应该是最相似的,但是通过如下代码将它们进行文本向量化后,会发现得到的结果是第2条新闻标题和第1条新闻标题最相似
第2条新闻标题和第1条新闻标题的欧氏距离更近,但其实第2条新闻标题和第3条新闻标题才是最相似的
这种因为文本长短造成的预测不精确可以通过余弦相似度来解决
余弦相似度是根据向量的夹角而非距离来判断相似度的
# 补充知识点:通过numpy库计算欧式距离
import numpy as np
dist = np.linalg.norm(df_test.iloc[0] - df_test.iloc[1])
dist
np.float64(1.0)
2. 余弦相似度的数学原理¶
如下图所示,在二维向量空间下可以使用两个向量夹角($\theta$)的余弦值($\cos \theta$)来表示两个向量的相似度,称为余弦相似度
余弦相似度的取值范围为 $0\sim 1$, 夹角越小,余弦值越接近1,则两个向量越靠近,说明它们越相似
很容易知道, 两个向量 $\pmb{a},\pmb{b}$ 的余弦相似度为:
$$ \cos <\pmb{a}, \pmb{b}> = \frac{\pmb{a}\cdot \pmb{b}}{|\pmb{a}| |\pmb{b}|} $$
容易理解, 借助余弦相似度能解决新闻标题的长度导致的分类不准确问题, 通过下图可以更好地理解欧式距离和余弦相似度的区别
虚线为欧式距离
夹角的余弦值为余弦相似度
a和b从余弦相似度上来看, 相似性远超欧式距离
3. 余弦相似度的Python代码实现¶
# 计算余弦相似度
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarities = cosine_similarity(df_test)
cosine_similarities
array([[1. , 0.8660254, 0.8660254], [0.8660254, 1. , 1. ], [0.8660254, 1. , 1. ]])
第 $i$ 行第 $i$ 列的数字表示第 $i$ 个数据和第 $j$ 个数据的余弦相似度
4. 利用余弦相似度进行模型优化¶
from sklearn.metrics.pairwise import cosine_similarity
cosine_similarities = cosine_similarity(df)
rounded_similarities = np.around(cosine_similarities, decimals=3)
print(rounded_similarities[1:10, 1:10])
[[1. 0.141 0.135 0.149 0.135 0.129 0. 0. 0.258] [0.141 1. 0.572 0.211 0.191 0.183 0.091 0.191 0.274] [0.135 0.572 1. 0.201 0.182 0.174 0.087 0.182 0.261] [0.149 0.211 0.201 1. 0.402 0.577 0.096 0.101 0.289] [0.135 0.191 0.182 0.402 1. 0.522 0.087 0.091 0.261] [0.129 0.183 0.174 0.577 0.522 1. 0.083 0.087 0.25 ] [0. 0.091 0.087 0.096 0.087 0.083 1. 0.696 0.083] [0. 0.191 0.182 0.101 0.091 0.087 0.696 1. 0.087] [0.258 0.274 0.261 0.289 0.261 0.25 0.083 0.087 1. ]]
接下来用KMeans算法对余弦相似度的计算结果进行聚类分群, 此时每条新闻标题的特征变量就是和其他新闻标题的余弦相似度
from sklearn.cluster import KMeans
kms = KMeans(n_clusters=10, random_state=123)
k_data = kms.fit_predict(cosine_similarities)
print(k_data[0:20])
[2 4 4 4 4 4 4 2 2 4 4 4 4 4 4 4 4 4 4 4]
# 把之前的分词结果转为数组
print(words[0:3])
words_ary = np.array(words)
print(words_ary[0:3])
['信托公司 2019 年 上半年 经营 业绩 概览', '首单 信托 型 企业 ABS 获批', '华能 贵 诚信 托孙磊 : 金融 科技 助力 打造 开放 信托 生态'] ['信托公司 2019 年 上半年 经营 业绩 概览' '首单 信托 型 企业 ABS 获批' '华能 贵 诚信 托孙磊 : 金融 科技 助力 打造 开放 信托 生态']
# 查看分类结果
import numpy as np
words_ary = np.array(words)
print(words_ary[k_data == 3][0:5]) # 可以把数字3改成其他数字看看效果
['装病 骗 政府 645 万 , 科技 远 不是 你 想 的 那样' '昔日 网游 第一股 迅游 科技 实控 人 被动 减持 , 商誉 压顶' 'ChinaJoy 2019 丨 ITheat 热点 科技 展台 人气 火爆 这些 精彩 看点 不 容错过' '兴证 王涵 : 第四次 科技 革命 是 机遇 还是 挑战 ?' '《 科技 志愿 服务 管理 办法 》 开始 实行 !']
补充知识点: 大数据分词--jieba库的使用¶
这节详细介绍jieba库的安装和使用方法,包括如何对长文本进行中文分词及词频统 计
中文分词(Chinese Word Segmentation)是指将一个中文序列切分成一个个单独的词
在英文的行文中,单词之间是以空格作为分界符的,而中文的词语则没有一个形式上的分界符,因此在分词这一层面,中文比英文要复杂得多
1. jieba库的安装与基本使用方法¶
在cmd中执行 pip install jieba
命令, 或再 Jupyter Notebook 中运行 !pip install jieba
代码即可安装jieba库
# 之前的演示
import jieba
word = jieba.cut('我爱北京天安门') # cut()函数分词得到的word不是一个列表, 而是一个迭代器generator(), 其实和列表很相似, 可以理解成一个 "隐身的列表", 不过其元素要通过for循环来访问, 所以后两行代码不能写成print(word)
for i in word:
print(i)
我 爱 北京 天安门
中文分词的核心操作就是jieba库的cut()函数的使用
上面演示的案例较为简单,下面对一个较长的文本内容进行中文分词及词频统计
2. 读取文本内容并进行分词¶
下面的文件是2017年的信托行业年度报告, 约20万字, 业务分析人员关心的是报告中哪些内容更重要,一个简单的实现手段就是看哪些词出现的频率最高,那么其对应内容的重要程度可能就越高
要达到这一目的,需要进行中文大数据分词和词频统计
对该txt文件中的文本内容进行分词的代码如下
import jieba
report = open('信托行业年度报告.txt', 'r').read()
words = jieba.cut(report)
for index, word in enumerate(words): # enumerate(words) 会返回一个包含索引和元素的元组,索引从 0 开始
if index >= 10:
break
print(word)
2017 年 信托业 面临 着 较为 复杂 的 外部环境 。
3. 按指定长度提取分词后的词¶
words = jieba.cut(report) # 这里得重新jieba.cut()一下,因为之前的words用过一次就被清空了
report_words = []
for word in words: # 将大于4个字的词语放入列表
if len(word) >= 4:
report_words.append(word)
print(report_words[0:20])
['2017', '外部环境', '结构调整', '新兴产业', '另一方面', '金融监管', '信托公司', '管理工作', '管理体系', '2017', '全面落实', '结构调整', '初见成效', '金融监管', '小康社会', '深远影响', '2017', '多管齐下', '长效机制', '长效机制']
4. 统计高频词汇¶
from collections import Counter
result = Counter(report_words)
print(result[0:20])
0
result = Counter(report_words).most_common(50) # 取最多的50组
print(result[0:20])
[('信托公司', 1391), ('2017', 577), ('2016', 184), ('金融机构', 148), ('投资信托', 108), ('基础产业', 91), ('2018', 87), ('风险管理', 82), ('工商企业', 77), ('QDII', 70), ('金融服务', 69), ('信息系统', 63), ('2015', 59), ('基础设施', 56), ('金融公司', 47), ('另一方面', 45), ('信托投资公司', 45), ('中国人民银行', 44), ('REITs', 39), ('金融业务', 38)]
补充知识点: 新闻爬取--百度新闻爬虫¶
百度新闻爬虫的相关知识点在作者的上一本书《Python金融大数据挖掘与分析全流程详解》
第三章“金融数据挖掘案例实战1”中有详细的讲解,这里结合了本书第二章pandas库的相关知识点将数据进行整合,并导出成Excel文件方便使用
因为核心知识点在上一本书已经描述的较为详细,所以这里就不在赘述,通过如下代码可以爬取百度新闻多页多个关键词
import requests
import re
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'}
def baidu(keyword, page): # 定义函数,方便之后批量调用
num = (page - 1) * 10
url = 'https://www.baidu.com/s?tn=news&rtt=4&bsst=1&cl=2&wd=' + keyword + '&pn=' + str(num)
res = requests.get(url, headers=headers).text # 通过requests库爬虫
# 正则提取信息
p_href = '<h3 class="c-title">.*?<a href="(.*?)"'
p_title = '<h3 class="c-title">.*?>(.*?)</a>'
p_info = '<p class="c-author">(.*?)</p>'
href = re.findall(p_href, res, re.S)
title = re.findall(p_title, res, re.S)
info = re.findall(p_info, res, re.S)
# 数据清洗
source = []
date = []
for i in range(len(title)):
title[i] = title[i].strip()
title[i] = re.sub('<.*?>', '', title[i])
info[i] = re.sub('<.*?>', '', info[i])
source.append(info[i].split(' ')[0])
date.append(info[i].split(' ')[1])
source[i] = source[i].strip()
date[i] = date[i].strip()
# 通过2.2.1节字典生成二维DataFrame表格
result = pd.DataFrame({'关键词': keyword, '标题': title, '网址': href, '来源': source, '日期': date})
return result
# 通过pandas库将数据进行整合并导出为Excel
import pandas as pd
df = pd.DataFrame()
keywords = ['华能信托', '人工智能']
for keyword in keywords:
for i in range(4): # 循环10遍,获取10页的信息
result = baidu(keyword, i+1)
df = pd.concat([df, result], ignore_index=True)
print(keyword + '第' + str(i+1) + '页爬取成功')
df.to_excel('新闻_new.xlsx') # 在代码所在文件夹生成EXCEL文件
华能信托第1页爬取成功 华能信托第2页爬取成功 华能信托第3页爬取成功 华能信托第4页爬取成功 人工智能第1页爬取成功 人工智能第2页爬取成功 人工智能第3页爬取成功 人工智能第4页爬取成功